# relay.py - serve repos over an httprelay service
# see http://code.google.com/p/httprelay/
#
# Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

'''serve repos over httprelay for when both parties are behind firewalls'''

import urllib, urllib2, base64
from StringIO import StringIO
from mercurial.i18n import _
from mercurial import cmdutil, hg, util, error
from mercurial.hgweb.hgweb_mod import hgweb
from mercurial.hgweb.request import wsgirequest

class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):
    def http_error_default(self, req, fp, code, msg, headers):
        result = urllib2.HTTPError(
            req.get_full_url(), code, msg, headers, fp)
        result.status = code
        return result

def _submit(host, url, headers=None, data=None):
    if url.startswith("/"):
        uri = host + url
    else:
        uri = host + "/" + url
    request = urllib2.Request(uri)
    if headers:
        for k, v in headers.iteritems():
            request.add_header(k, v)
    if data:
        request.add_data(data)
    opener = urllib2.build_opener(DefaultErrorHandler())
    return opener.open(request)

class Response:
    pass

def _internal(ui, hgwebobj, method, path, headers, data):
    ui.status("%s %s\n" % (method, path))
    pathcomps = path.split("?", 1)
    if len(pathcomps) > 1:
        pathinfo, querystring = pathcomps
    else:
        pathinfo = path
        querystring = ""
    env = {
           "wsgi.version": (1,0),
           "wsgi.input": StringIO(data),
           "wsgi.errors": None,
           "wsgi.multithread": False,
           "wsgi.multiprocess": False,
           "wsgi.run_once": False,
           "REQUEST_METHOD": method,
           "QUERY_STRING": querystring,
           "PATH_INFO": pathinfo,
           "REQUEST_URI": path,
           "SCRIPT_NAME": "",
           "SERVER_PORT": "?",
           "SERVER_NAME": "httprelay",
           }
    for k,v in headers.iteritems():
        k = k.replace("-", "_").upper()
        env[k] = v
        env["HTTP_" + k] = v
    rsp = Response()
    rsp.headers = {}
    rsp.status = None
    rsp.body = None
    def start(status, headers, exc_info=None):
        rsp.headers = dict(headers)
        rsp.status = status
    rsp.body = "".join(hgwebobj.run_wsgi(wsgirequest(env, start)))
    return rsp

def _handle(ui, hgwebobj, host, req):
    ui.note(_("querying local server\n"))
    info, rest = req.split("\n", 1)
    _magic, id, method, path = info.split(" ", 3)
    headers, data = rest.split("\n-\n", 1)
    headers = dict(h.split(": ", 1) for h in headers.splitlines())
    rsp = _internal(ui, hgwebobj, method, path, headers, data)

    ui.note(_("forwarding response to relay host\n"))
    headers = rsp.headers
    body = rsp.body
    type = headers.get("Content-Type").lower()
    if not type or not type.startswith("text/"):
        ui.note(_("Encoding\n"))
        body = base64.b64encode(body)
    headers = "".join("%s: %s\n" % (k, v) for k, v in headers.iteritems())
    data = urllib.urlencode([("headers", headers), ("body", body)])
    _submit(host, "/httprelay/fwd/%s" % id, data=data)

def relay(ui, repo, **opts):
    """export the repository via httprelay (use when behind a firewall)

    Start a local HTTP repository browser and pull server. This server communicates
    with a relay service hosted on Google's AppEngine (appspot.com), instead
    of listening on a local port (see http://code.google.com/p/httprelay/).

    Specify the relay host with the --host argument. This is typically
    "http://our-secret.our-app.appspot.com/", where "our-app" is the name of your
    Google AppEngine instance running the httprelay service, and "our-secret" is an
    arbitrary string the that client will have to use to access the reflected server.

    With --allow-push, the server will accept incoming pushes without SSL from
    anyone (--config 'web.allow_push=*' --config 'web.push_ssl=false').

    Otherwise "hg relay" behaves very much like "hg serve".
    """
    baseui = repo and repo.baseui or ui
    optlist = ("name templates style"
               " accesslog errorlog webdir_conf certificate encoding")
    def setcfg(section, option, value):
        baseui.setconfig(section, option, value)
        if (repo is not None) and (repo.ui != baseui):
            repo.ui.setconfig(section, option, value)
    for o in optlist.split():
        if opts.get(o, None):
            setcfg("web", o, str(opts[o]))

    if opts.get("allow_push"):
        setcfg("web", "allow_push", "*")
        setcfg("web", "push_ssl", "false")

    if repo is None and not ui.config("web", "webdir_conf"):
        raise error.RepoError(_("There is no Mercurial repository here"
                                " (.hg not found)"))

    host = opts.get("host")
    if not host:
        raise error.Abort(_("Must specify --host"))
    if host.endswith("/"):
        host = host[:-1]

    class service(object):

        def init(self):
            util.set_signal_handler()
            self.hgwebobj = hgweb(hg.repository(repo.ui, repo.root))
            if not ui.verbose:
                return
            ui.status(_('relaying via %s\n') % host)

        def run(self):
            hgwebobj = self.hgwebobj
            while True:
                ui.note(_("polling relay host\n"))
                req = _submit(host, "/httprelay/poll").read()
                if req and req.startswith("httprelay "):
                    _handle(ui, hgwebobj, host, req)

    service = service()
    cmdutil.service(opts, initfn=service.init, runfn=service.run)


cmdtable = {
    'relay':
        (relay,
         [('H', 'host', '', _('URL of the httprelay service host')),
          ('A', 'accesslog', '', _('name of access log file to write to')),
          ('E', 'errorlog', '', _('name of error log file to write to')),
          ('n', 'name', '',
           _('name to show in web pages (default: working directory)')),
          ('t', 'templates', '', _('web templates to use')),
          ('', 'style', '', _('template style to use')),
          ('d', 'daemon', None, _('run server in background')),
          ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
          ('', 'pid-file', '', _('name of file to write process ID to')),
          ('', 'allow-push', None, _('allow pushing')),
         ],
         _('[OPTION]...')),
}
